from utils.misc import INF, check_type
from utils.prettyrepr import prettify_class

from .plugin import Plugin
from ..signals import SIGNALS
from ..statemachine import ACTION


@prettify_class
class SolverLimitManager(object):
    """
    This object manages the limits imposed on a solver. Limits are special-purpose solver plugins
    that are used to interrupt the solver's main loop when specific conditions are met, such as
    the cpu time reaching a certain threshold. Instead of being managed by the solver's plugin
    manager like other "regular" plugins, these are managed separately because they are replaced
    frequently by new limits.
    """
    def __init__(self, solver):
        self.solver = solver
        self.limits = set()

    def __info__(self):
        return self.limits

    def __len__(self):
        return len(self.limits)

    def __iter__(self):
        return iter(self.limits)

    def clear(self):
        for limit in self.limits:
            limit.uninstall()
        self.limits.clear()

    reset = clear

    def add(self, limit):
        check_type(limit, Limit)
        if limit not in self.limits:
            self.limits.add(limit)
            limit.install(self.solver)
            return True
        return False

    def extend(self, limits):
        for limit in limits:
            self.add(limit)

    def update(self, limits):
        """This method works like extend(), but instead of blindly adding all limits, it discards
        old limits of the same type as any of the argument limits, e.g. if a cpu limit is passed
        in the argument list, all previous cpu limits (type must match exactly) are discarded and
        replaced by the new cpu limit."""
        # build a {cls: [limits]} mapping
        type_map = {}
        for limit in self.limits:
            cls = type(limit)
            try:
                type_map[cls].append(limit)
            except KeyError:
                type_map[cls] = [limit]
        # now we apply the new limits, removing any previous limits of the same type
        for limit in limits:
            check_type(limit, Limit)
            cls = type(limit)
            if cls in type_map:
                for prev_limit in type_map[cls]:
                    prev_limit.uninstall()
                    self.limits.remove(prev_limit)
            self.add(limit)
            type_map[cls] = [limit]

    def set(self, limits):
        """Completely replaces any previous limits with the argument limits."""
        self.clear()
        self.extend(limits)


class Limit(Plugin):
    """
    Each of these plugins adds an interrupt to the solver when a particular limit is reached,
    e.g. cpu time, iterations, solutions, etc.  In addition to interrupting the solver, a limit
    may emit a signal on the solver's channel.  Limits may be passed to the solver's run() method,
    and are uninstalled and discarded when the solver is run() again with new limits.
    """
    activation_action = ACTION.PAUSE
    activation_signal = None

    def __init__(self, rel=INF, abs=INF):
        Plugin.__init__(self)
        self.rel = rel
        self.abs = abs
        self.limit = None

    def __info__(self):
        return ", ".join("{}={}".format(attr, getattr(self, attr))
                         for attr in ("rel", "abs", "limit"))

    @classmethod
    def abs(cls, abs):
        return cls(abs=abs)

    @classmethod
    def rel(cls, rel):
        return cls(rel=rel)

    @property
    def activation_message(self):
        return "{} reached ({} >= {})".format(type(self).__name__, self.get_value(), self.limit)

    def install(self, solver):
        Plugin.install(self, solver)
        self.limit = min(self.abs, self.rel + self.get_value())

    def check(self, listener):
        if self.get_value() >= self.limit:
            if self.activation_signal is not None:
                self.solver.channel.emit(self.activation_signal)
            self.solver.interrupts.add(self.activation_message, self.activation_action)

    def get_value(self, solver):
        raise NotImplementedError()


class CpuLimit(Limit):
    signal_map = {SIGNALS.ITERATION_FINISHED: "check"}
    activation_signal = SIGNALS.CPU_LIMIT_REACHED

    def install(self, solver):
        Limit.install(self, solver)
        solver.cpu.limit = self.limit

    def get_value(self):
        return self.solver.cpu.total


class ItersLimit(Limit):
    signal_map = {SIGNALS.ITERATION_FINISHED: "check"}
    activation_signal = SIGNALS.ITERATION_LIMIT_REACHED

    def install(self, solver):
        Limit.install(self, solver)
        solver.iters.limit = self.limit

    def get_value(self):
        return self.solver.iters.total


class SolutionsLimit(Limit):
    signal_map = {SIGNALS.SOLUTION_ADDED: "check"}
    activation_signal = SIGNALS.SOLUTION_LIMIT_REACHED

    def get_value(self):
        return len(self.solver.solutions)


class GapLimit(Limit):
    signal_map = {SIGNALS.GAP_CHANGED: "check"}
    activation_signal = SIGNALS.GAP_LIMIT_REACHED

    def get_value(self):
        return self.solver.gap


Limit.Cpu = CpuLimit
Limit.Iters = ItersLimit
Limit.Solutions = SolutionsLimit
Limit.Gap = GapLimit
